package com.ctgu.zmc.stresstest.common.utils;


import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.json.JSONUtil;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.concurrent.TimeUnit;

/**
 * redis实现分布式锁
 *
 * @author ZMC
 */
@Component
public class RedisDistributedLock {

    /**
     * 线程标识的前缀，用来识别锁被哪个线程获取了，避免分布式锁超时释放而发生误删
     * <p>
     * 声明为static final类型的原因:
     * static保证ID_PREFIX在项目启动时赋值
     * final保证ID_PREFIX不会变，这样可以保证同一个JVM进程只有一个UUID，以此区分不同的JVM进程
     * 即每个JVM进程的UUID不同，再加上同一个JVM进程下的线程id也不同，这样就可以用来标识所有JVM进程下的所有线程了
     * </p>
     */
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";

    /**
     * 分布式锁-释放锁: Lua脚本，保证多条redis命令执行的原子性
     */
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;

    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("/luaScript/unLuck.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    private final StringRedisTemplate stringRedisTemplate;

    public RedisDistributedLock(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 尝试获取分布式锁
     *
     * @param key   锁的key
     * @param value 任意数据，一般是线程的标识，用来识别锁被哪个线程获取了
     * @param time  过期时间
     * @param unit  时间单位
     * @return true:获取成功; false:获取失败
     */
    public boolean tryLock(String key, Object value, Long time, TimeUnit unit) {
        Boolean flag;
        if (value == null) {
            // UUID + 当前线程id作为线程标识
            String threadTag = ID_PREFIX + Thread.currentThread().getId();
            flag = stringRedisTemplate.opsForValue().setIfAbsent(key, threadTag, time, unit);
            return BooleanUtil.isTrue(flag);
        }
        flag = stringRedisTemplate.opsForValue().setIfAbsent(key, JSONUtil.toJsonStr(value), time, unit);
        // 不能直接返回flag，因为可能存在空指针异常
        return BooleanUtil.isTrue(flag);
    }

    public boolean tryLock(String key, Long time, TimeUnit unit) {
        return this.tryLock(key, null, time, unit);
    }


    /**
     * 释放分布式锁: 不能保证 线程标识匹配 和 释放锁 这两个操作的原子性
     *
     * @param key 锁的key
     */
    public void unLock(String key) {
        // 1.获取当前线程标识
        String nowThreadTag = ID_PREFIX + Thread.currentThread().getId();
        // 2.对比锁的线程标识
        String lockThreadTag = stringRedisTemplate.opsForValue().get(key);
        // 3.标识相同，说明锁是当前线程获取的，可以释放锁
        if (nowThreadTag.equals(lockThreadTag)) {
            // 删除锁对应的KV键值对
            stringRedisTemplate.delete(key);
        }
        // 4.标识不同，说明锁是其它线程获取的，不能释放其它线程的锁，避免误删
    }

    /**
     * 释放分布式锁: 利用Lua脚本保证多条命令的原子性，优化分布式锁
     *
     * @param key 锁的key
     */
    public void unLockWithLua(String key) {
        // 1.获取当前线程标识
        String nowThreadTag = ID_PREFIX + Thread.currentThread().getId();
        // 2.调用Lua脚本: 对比锁的线程标识 + 释放锁，可以保证原子性
        stringRedisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(key), nowThreadTag);
    }


}
